fix(driver-sql): canonical ISO-8601-Z for user NOW()-default temporal fields on SQLite#2346
Merged
Merged
Conversation
… fields on SQLite A user-declared Field.datetime (or date/time) with defaultValue:'NOW()' took the knex.fn.now() -> CURRENT_TIMESTAMP column default on SQLite, storing a timezone-naive 'YYYY-MM-DD HH:MM:SS' that Date.parse reads as LOCAL time (the same class of bug ADR-0074 fixed for the builtin audit columns, scoped out there for user fields). Worse, the same column mixed storage: an explicit Date is bound by better-sqlite3 as INTEGER epoch ms while an omitted value took the naive TEXT default. - createColumn's NOW() default now emits a per-type canonical via strftime (datetime ISO-8601-Z, date YYYY-MM-DD, time HH:MM:SS.fff) through the new nowColumnDefault helper. - formatOutput folds every Field.datetime storage form (INTEGER epoch ms, canonical ISO-Z, legacy naive TEXT) to one canonical ISO-8601-Z instant (normalizeSqliteDatetimeOutput), repairing legacy rows on read with no data migration. SQLite-only; Postgres/MySQL keep native now() and are unaffected. Follow-up to ADR-0074 (no new ADR), rebased onto it. normalizeSqliteDatetimeOutput reuses ADR-0074's repairNaiveUtcAuditTimestamp for the string shapes and adds the INTEGER epoch-ms / Date folding, so a datetime-typed audit column now also presents as canonical ISO-Z (consistent with every datetime field) — ADR-0074's "datetime-typed created_at reads as epoch-ms number" test is updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
added a commit
that referenced
this pull request
Jun 26, 2026
…ad (SQLite) (#2347) Field.time is a tz-naive time-of-day, not an instant (#2004). A defaultValue:'NOW()' time column historically took the full SQLite CURRENT_TIMESTAMP default, so a defaulted/legacy row read back a full 'YYYY-MM-DD HH:MM:SS' timestamp instead of a time-of-day. formatOutput now repairs a Field.time value to just its time portion (toTimeOnly): a legacy full timestamp (or a full ISO value that leaked into the column) is sliced to HH:MM[:SS[.fff]], while a value already stored as a bare time-of-day is left untouched. Deliberately NARROW + read-only (no write/filter counterpart) so it introduces no write/read asymmetry and preserves exact round-trips for bare time-of-day values (the field-zoo f_time guard). Runs for every dialect (a native TIME column already returns a time-of-day → no-op). Adds a timeFields registry + registration mirroring dateFields/datetimeFields. Completes the temporal read normalization alongside #2346: datetime -> ISO-8601-Z, date -> YYYY-MM-DD, time -> wall-clock time-of-day. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The ADR-0074 follow-up it explicitly scoped out: a user-declared
Field.datetime(ordate/time) withdefaultValue: 'NOW()'gotcol.defaultTo(knex.fn.now())increateColumn. On SQLite that compiles toCURRENT_TIMESTAMP— a timezone-naive'YYYY-MM-DD HH:MM:SS'(no millis, no zone) thatDate.parsereads as local time, so the stored UTC wall-clock shifts by the host offset on a non-UTC runtime. The same class of bug ADR-0074 fixed forcreated_at/updated_at.Worse, the same datetime column mixed storage: an explicit JS
Dateis bound by better-sqlite3 as INTEGER epoch ms, while an omitted value took the naive TEXT default — one column holding both.Fix (SQLite-only; Postgres/MySQL keep native
now()and are unaffected)nowColumnDefault(type)emits a per-type canonical default viastrftime: datetime → ISO-8601-Z(2026-06-26T10:34:13.891Z), date →YYYY-MM-DD, time →HH:MM:SS.fff.formatOutputfolds everyField.datetimestorage form (INTEGER epoch ms, canonical ISO-Z, legacy naive TEXT → interpreted as UTC) to one canonical ISO-8601-Zinstant vianormalizeSqliteDatetimeOutput. Idempotent, total, repairs legacy rows on read with no data migration.A DDL default only governs newly-created columns, so existing-table coverage comes from the read normalization (not app-side stamping like ADR-0074's audit fix) — an explicit
Datestays INTEGER regardless, so the INTEGER-vs-TEXT split is inherent to SQLite and resolved at the read boundary. Decision rationale lives in the changeset + thenowColumnDefaultcomment (no new ADR).Reconciliation with ADR-0074 (#2342, just merged)
Rebased onto ADR-0074.
normalizeSqliteDatetimeOutputreuses itsrepairNaiveUtcAuditTimestampfor the string shapes (single source of the zone-naive→UTC rules) and adds the INTEGER epoch-ms /Datefolding. Consequently adatetime-typed audit column now also presents as canonical ISO-Z— more consistent than before (ADR-0074's own string-typed audit columns already present as ISO-Z). ADR-0074's "datetime-typedcreated_atreads as epoch-ms number" test is updated to expect the canonical string.Testing
@objectstack/driver-sql: 227 passed (incl. 10 new insql-driver-user-datetime-default-format.test.ts; ADR-0074's reconciled test green).@objectstack/service-analytics: 164 passed — analytics SQL bucketing reads these columns viastrftime, bypassingformatOutput; verified ISO-ZTEXT buckets identically to the old naive TEXT, so no regression.🤖 Generated with Claude Code